﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="XibControllerGenerator.cs" company="Roman '4ux-nbIx' Novitsky">
//   Copyright (c) 2012 Roman '4ux-nbIx' Novitsky
// </copyright>
// <summary>
//   Defines the XibControllerGenerator type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace MonoDevAssist
{
  #region Namespace Imports

  using System;
  using System.Collections.Generic;
  using System.Globalization;
  using System.IO;
  using System.Linq;
  using System.Text;
  using System.Xml.Linq;

  using EnvDTE;

  #endregion


  internal sealed class XibControllerGenerator
  {
    #region Constants and Fields

    private string _controllerCode;
    private string _controllerCodeBehindCode;

    #endregion


    #region Public Methods

    /// <summary>
    /// Generates the controller.
    /// </summary>
    /// <param name="xibFileNames">The xib file names.</param>
    /// <param name="dte">The DTE.</param>
    /// <exception cref="Exception">Failed to parse XIB: Objects element not found.</exception>
    public void GenerateController(IEnumerable<string> xibFileNames, DTE dte)
    {
      var xibInfos = new List<XibFileInfo>();

      foreach (string xibFileName in xibFileNames)
      {
        var fileInfo = new XibFileInfo { FileName = xibFileName };

        var document = XDocument.Load(xibFileName);

        XElement objectsElement = (from item in document.Descendants()
                                   where
                                     (string)item.Attribute("class") == "IBObjectContainer"
                                     && (string)item.Attribute("key") == "IBDocument.Objects"
                                   select item).FirstOrDefault();

        if (objectsElement == null)
        {
          throw new Exception("Failed to parse XIB: Objects element not found.");
        }

        XElement flattenedPropertiesElement = (from item in objectsElement.Elements()
                                               where
                                                 (string)item.Attribute("class") == "NSMutableDictionary"
                                                 && (string)item.Attribute("key") == "flattenedProperties"
                                               select item).FirstOrDefault();

        if (flattenedPropertiesElement == null)
        {
          throw new Exception("Failed to parse XIB: flattenedProperties element not found.");
        }

        IEnumerable<Tuple<string, string>> flattenedProperties = ParseDictionary(flattenedPropertiesElement);

        XElement rootObjectsElement = (from item in document.Descendants()
                                       where
                                         (string)item.Attribute("class") == "NSMutableArray"
                                         && (string)item.Attribute("key") == "IBDocument.RootObjects"
                                       select item).FirstOrDefault();

        if (rootObjectsElement == null)
        {
          throw new Exception("Failed to parse XIB: Root Objects element not found.");
        }

        IEnumerable<Tuple<string, string>> proxyObjects = from item in rootObjectsElement.Elements("object")
                                                          let id = (string)item.Attribute("id")
                                                          let name = (from e in item.Elements("string")
                                                                      where
                                                                        (string)e.Attribute("key")
                                                                        == "IBProxiedObjectIdentifier"
                                                                      select (string)e).FirstOrDefault()
                                                          where
                                                            (string)item.Attribute("class") == "IBProxyObject"
                                                            && !string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(name)
                                                          select new Tuple<string, string>(id, name);

        IEnumerable<Tuple<string, string>> uiElements = from item in rootObjectsElement.Descendants("object")
                                                        let id = (string)item.Attribute("id")
                                                        let uiElementClassName = (string)item.Attribute("class")
                                                        where
                                                          uiElementClassName != "IBProxyObject"
                                                          && item.Parent != rootObjectsElement
                                                          && !string.IsNullOrEmpty(id)
                                                          && !string.IsNullOrEmpty(uiElementClassName)
                                                          && uiElementClassName.Length > 2
                                                        select
                                                          new Tuple<string, string>(id, uiElementClassName.Substring(2));

        IEnumerable<Tuple<string, string>> objectRecords = from item in document.Descendants("object")
                                                           let id = (from e in item.Elements("int")
                                                                     where (string)e.Attribute("key") == "objectID"
                                                                     select (string)e).FirstOrDefault()
                                                           let reference = (from e in item.Elements("reference")
                                                                            where (string)e.Attribute("key") == "object"
                                                                            select (string)e.Attribute("ref")).FirstOrDefault()
                                                           where
                                                             (string)item.Attribute("class") == "IBObjectRecord"
                                                             && !string.IsNullOrEmpty(id)
                                                             && !string.IsNullOrEmpty(reference)
                                                           select new Tuple<string, string>(id, reference);

        Tuple<string, string> filesOwner = proxyObjects.FirstOrDefault(p => p.Item2 == "IBFilesOwner");

        if (filesOwner == null)
        {
          throw new Exception("Failed to parse XIB: File's Owner element not found.");
        }

        Tuple<string, string> filesOwnerObjectRecord = objectRecords.FirstOrDefault(r => r.Item2 == filesOwner.Item1);

        IEnumerable<Tuple<string, string>> outletConnections = (from item in document.Descendants("object")
                                                                let labelElement = (from e in item.Elements("string")
                                                                                    where
                                                                                      (string)e.Attribute("key")
                                                                                      == "label"
                                                                                    select e).FirstOrDefault()
                                                                let sourceReference =
                                                                  (from e in item.Elements("reference")
                                                                   where (string)e.Attribute("key") == "source"
                                                                   select (string)e.Attribute("ref")).FirstOrDefault()
                                                                let destinationReference =
                                                                  (from e in item.Elements("reference")
                                                                   where (string)e.Attribute("key") == "destination"
                                                                   select (string)e.Attribute("ref")).FirstOrDefault()
                                                                where
                                                                  (string)item.Attribute("class")
                                                                  == "IBCocoaTouchOutletConnection"
                                                                  && labelElement != null
                                                                  && !string.IsNullOrEmpty((string)labelElement)
                                                                  && sourceReference == filesOwner.Item1
                                                                  &&
                                                                  uiElements.Any(e => e.Item1 == destinationReference)
                                                                select
                                                                  new Tuple<string, string>(
                                                                  (string)labelElement, 
                                                                  uiElements.First(e => e.Item1 == destinationReference).Item2)).ToList();

        if (filesOwnerObjectRecord == null)
        {
          throw new Exception("Failed to parse XIB: File's Owner Object Record not found.");
        }

        fileInfo.ClassName =
          flattenedProperties.Where(p => p.Item1 == filesOwnerObjectRecord.Item1 + ".CustomClassName").Select(
            t => t.Item2).FirstOrDefault();

        if (fileInfo.ClassName == null)
        {
          continue;
        }

        XElement classDescription = (from item in document.Descendants("object")
                                     let classDescriptionClassName = (from e in item.Elements("string")
                                                                      where (string)e.Attribute("key") == "className"
                                                                      select (string)e).FirstOrDefault()
                                     where
                                       (string)item.Attribute("class") == "IBPartialClassDescription"
                                       && classDescriptionClassName == fileInfo.ClassName
                                     select item).FirstOrDefault();

        if (classDescription != null)
        {
          fileInfo.BaseClassName = (from e in classDescription.Elements("string")
                                    let hasNameAttribute = (string)e.Attribute("key") == "superclassName"
                                    where hasNameAttribute
                                    select (string)e).FirstOrDefault();

          var outletInfos = (from item in classDescription.Descendants("object")
                             let nameElement = (from e in item.Elements("string")
                                                where (string)e.Attribute("key") == "name"
                                                select e).FirstOrDefault()
                             let classNameElement = (from e in item.Elements("string")
                                                     where (string)e.Attribute("key") == "candidateClassName"
                                                     select e).FirstOrDefault()
                             where
                               (string)item.Attribute("class") == "IBToOneOutletInfo" && nameElement != null
                               && !string.IsNullOrEmpty((string)nameElement) && classNameElement != null
                               && !string.IsNullOrEmpty((string)classNameElement)
                             select new Tuple<string, string>((string)nameElement, (string)classNameElement)).ToList();

          var outlets = new List<Tuple<string, string>>();

          foreach (Tuple<string, string> outletConnection in outletConnections)
          {
            Tuple<string, string> tuple = outletInfos.FirstOrDefault(i => i.Item1 == outletConnection.Item1);

            if (tuple == null)
            {
              outlets.Add(outletConnection);
              continue;
            }

            outlets.Add(new Tuple<string, string>(outletConnection.Item1, tuple.Item2));
          }

          fileInfo.Outlets = outlets;
        }
        else
        {
          fileInfo.BaseClassName = "UIViewController";
          fileInfo.Outlets = outletConnections;
        }

        fileInfo.EventConnections = (from item in document.Descendants("object")
                                     let labelElement = (from e in item.Elements("string")
                                                         where (string)e.Attribute("key") == "label"
                                                         select e).FirstOrDefault()
                                     where
                                       (string)item.Attribute("class") == "IBCocoaTouchEventConnection"
                                       && labelElement != null && !string.IsNullOrEmpty((string)labelElement)
                                     select (string)labelElement).Distinct();

        xibInfos.Add(fileInfo);
      }

      var activeProject = dte.GetActiveProject();
      var defaultNamespace = activeProject.GetDefaultNamespace();
      string projectFileName = activeProject.FileName;

      var xibDirectory = Path.GetDirectoryName(projectFileName);

      if (xibDirectory == null)
      {
        throw new Exception("Unexpected error occurred.");
      }

      foreach (IGrouping<string, XibFileInfo> xibsByClassName in xibInfos.GroupBy(i => i.ClassName))
      {
        var xibsByBaseClassName = xibsByClassName.GroupBy(x => x.BaseClassName).ToList();

        if (xibsByBaseClassName.Count() > 1)
        {
          Utilities.ShowMessageBox(
            "Cannot generate controllers for selected XIBs: some XIBs reference controllers with same class names, but different base classes.");

          continue;
        }

        string className = xibsByClassName.Key;
        string baseClassName = xibsByBaseClassName.First().Key;
        IEnumerable<string> eventConnections = xibsByClassName.SelectMany(x => x.EventConnections).Distinct();
        IEnumerable<Tuple<string, string>> outlets = xibsByClassName.SelectMany(x => x.Outlets).Distinct();

        GenerateControllerCode(
          className, baseClassName, defaultNamespace, xibsByClassName.Select(x => x.FileName).ToList());
        GenerateControllerCodeBehindCode(className, defaultNamespace, outlets, dte, eventConnections);

        string controllerFileName = className + ".cs";
        controllerFileName = Path.Combine(xibDirectory, controllerFileName);

        ProjectItem controllerProjectItem = CreateFile(controllerFileName, _controllerCode, activeProject.ProjectItems);

        string codeBehindFileName = className + ".designer.cs";
        codeBehindFileName = Path.Combine(xibDirectory, codeBehindFileName);

        CreateFile(codeBehindFileName, _controllerCodeBehindCode, controllerProjectItem.ProjectItems);
      }
    }

    #endregion


    #region Methods

    private ProjectItem CreateFile(string controllerFileName, string fileContents, ProjectItems projectItems)
    {
      FileStream fileStream = null;

      try
      {
        fileStream = new FileStream(controllerFileName, FileMode.CreateNew);

        using (var writer = new StreamWriter(fileStream))
        {
          fileStream = null;
          writer.Write(fileContents);
        }
      }
      finally
      {
        if (fileStream != null)
        {
          fileStream.Dispose();
        }
      }

      return projectItems.AddFromFile(controllerFileName);
    }


    /// <summary>
    /// Generates the controller code.
    /// </summary>
    /// <param name="className">Name of the class.</param>
    /// <param name="baseClassName">Name of the <c>base</c> class.</param>
    /// <param name="namespaceName">The <c>namespace</c> name.</param>
    /// <param name="xibFileNames">The xib file names.</param>
    /// <exception cref="Exception">Unexpected error.</exception>
    private void GenerateControllerCode(
      string className, 
      string baseClassName, 
      string namespaceName, 
      List<string> xibFileNames)
    {
      string template;

      Stream templateFileStream = null;

      try
      {
        if (xibFileNames.Count() == 1)
        {
          templateFileStream =
            typeof(MonoTouchFlavePackageProject).Assembly.GetManifestResourceStream(
              "MonoDevAssist.Resources.SingleXibControllerFileTemplate.txt");
        }
        else
        {
          templateFileStream =
            typeof(MonoTouchFlavePackageProject).Assembly.GetManifestResourceStream(
              "MonoDevAssist.Resources.MultipleXibsControllerFileTemplate.txt");
        }

        if (templateFileStream == null)
        {
          throw new Exception("Unexpected error.");
        }

        using (var reader = new StreamReader(templateFileStream))
        {
          templateFileStream = null;
          template = reader.ReadToEnd();
        }
      }
      finally
      {
        if (templateFileStream != null)
        {
          templateFileStream.Dispose();
        }
      }

      string xibName = Path.GetFileNameWithoutExtension(xibFileNames.First());

      _controllerCode =
        template.Replace("$(namespace)", namespaceName).Replace("$(className)", className).Replace(
          "$(baseClassName)", baseClassName).Replace("$(xibName)", xibName);
    }


    /// <summary>
    ///   Generates the controller code behind code.
    /// </summary>
    /// <param name="className"> Name of the class. </param>
    /// <param name="namespaceName"> Name of the <c>namespace</c> . </param>
    /// <param name="outlets"> The <c>outlets</c> . </param>
    /// <param name="dte"> The DTE. </param>
    /// <param name="eventConnections"> The event connections. </param>
    /// <exception cref="Exception">Unexpected error.</exception>
    private void GenerateControllerCodeBehindCode(
      string className,
      string namespaceName,
      IEnumerable<Tuple<string, string>> outlets,
      _DTE dte,
      IEnumerable<string> eventConnections)
    {
      string template;

      Stream templateFileStream = null;

      try
      {
        templateFileStream =
          typeof(MonoTouchFlavePackageProject).Assembly.GetManifestResourceStream(
            "MonoDevAssist.Resources.ControllerCodeBehindFileTemplate.txt");

        if (templateFileStream == null)
        {
          throw new Exception("Unexpected error.");
        }

        using (var reader = new StreamReader(templateFileStream))
        {
          templateFileStream = null;
          template = reader.ReadToEnd();
        }
      }
      finally
      {
        if (templateFileStream != null)
        {
          templateFileStream.Dispose();
        }
      }

      var outletsCode = new StringBuilder();
      var outletsDisposeCode = new StringBuilder();

      var activeProject = dte.GetActiveProject();
      IEnumerable<string> platforms =
        ((Array)activeProject.ConfigurationManager.SupportedPlatforms).Cast<string>().ToList();

      foreach (Tuple<string, string> outlet in outlets)
      {
        string outletProperty;

        if (dte.GetCodeType("MonoTouch.UIKit." + outlet.Item2, platforms) != null)
        {
          outletProperty = "MonoTouch.UIKit." + outlet.Item2 + " " + outlet.Item1;
        }
        else
        {
          var codeClass = dte.GetClass(outlet.Item2, platforms).FirstOrDefault();

          if (codeClass != null)
          {
            outletProperty = codeClass.FullName + " " + outlet.Item1;
          }
          else
          {
            outletProperty = outlet.Item2 + " " + outlet.Item1;
          }
        }

        outletsCode.AppendLine("		[Outlet]");
        outletsCode.AppendFormat(CultureInfo.InvariantCulture, "		{0} {{ get; set; }}\n", outletProperty);
        outletsCode.AppendLine();

        outletsDisposeCode.AppendFormat(CultureInfo.InvariantCulture, "			if ({0} != null) {{\n", outlet.Item1);
        outletsDisposeCode.AppendFormat(CultureInfo.InvariantCulture, "				{0}.Dispose ();\n", outlet.Item1);
        outletsDisposeCode.AppendFormat(CultureInfo.InvariantCulture, "				{0} = null;\n", outlet.Item1);
        outletsDisposeCode.AppendLine("			}");
        outletsDisposeCode.AppendLine();
      }

      var eventConnectionsCode = new StringBuilder();

      foreach (string eventConnection in eventConnections)
      {
        eventConnectionsCode.AppendLine();
        eventConnectionsCode.AppendLine();
        eventConnectionsCode.AppendFormat(CultureInfo.InvariantCulture, "\t\t[Export(\"{0}\")]\n", eventConnection);

        string handlerName = eventConnection;
        string handlerParameters = string.Empty;

        var paramenetrsCount = handlerName.Count(c => c == ':');

        if (paramenetrsCount > 0)
        {
          handlerName = handlerName.Replace(":", string.Empty);

          if (paramenetrsCount == 1)
          {
            handlerParameters = "NSObject sender";
          }
          else if (paramenetrsCount == 2)
          {
            handlerParameters = "NSObject sender, UIEvent event";
          }
        }

        eventConnectionsCode.AppendFormat(
          CultureInfo.InvariantCulture, "\t\tpartial void {0}({1});\n", handlerName, handlerParameters);
        eventConnectionsCode.AppendLine();
      }

      _controllerCodeBehindCode =
        template.Replace("$(namespace)", namespaceName).Replace("$(className)", className).Replace(
          "$(outlets)", outletsCode.ToString()).Replace("$(outletsDisposeCode)", outletsDisposeCode.ToString()).Replace(
            "$(eventConnections)", eventConnectionsCode.ToString());
    }


    private IEnumerable<Tuple<string, string>> ParseDictionary(XContainer dictionaryElement)
    {
      XElement keysElement = (from item in dictionaryElement.Elements("object")
                              where (string)item.Attribute("key") == "dict.sortedKeys"
                              select item).FirstOrDefault();

      XElement valuesElement = (from item in dictionaryElement.Elements("object")
                                where (string)item.Attribute("key") == "dict.values"
                                select item).FirstOrDefault();

      if (keysElement == null || valuesElement == null)
      {
        List<Tuple<string, string>> list = (from item in dictionaryElement.Elements("string")
                                            let key = (string)item.Attribute("key")
                                            where !string.IsNullOrEmpty(key) && !string.IsNullOrEmpty((string)item)
                                            select new Tuple<string, string>(key, (string)item)).ToList();

        return list.Count == 0 ? null : list;
      }

      List<string> keys = (from item in keysElement.Elements("string")
                           select (string)item).ToList();

      List<string> values = (from item in valuesElement.Elements()
                             where (string)item.Attribute("key") != "EncodedWithXMLCoder"
                             select (string)item).ToList();

      var dictionary = new List<Tuple<string, string>>();

      for (int i = 0; i < keys.Count(); i++)
      {
        dictionary.Add(new Tuple<string, string>(keys[i], values[i]));
      }

      return dictionary;
    }

    #endregion


    private sealed class XibFileInfo
    {
      #region Properties

      public string BaseClassName
      {
        get;
        set;
      }

      public string ClassName
      {
        get;
        set;
      }

      public IEnumerable<string> EventConnections
      {
        get;
        set;
      }

      public string FileName
      {
        get;
        set;
      }

      public IEnumerable<Tuple<string, string>> Outlets
      {
        get;
        set;
      }

      #endregion
    }
  }
}